package com.bitsfromspace.ubertorrent.tracker;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Random;

import com.bitsfromspace.ubertorrent.Sha1Hash;
import com.bitsfromspace.ubertorrent.peer.PeerAddress;
import com.bitsfromspace.ubertorrent.peer.PeerAddressTest;
import com.bitsfromspace.ubertorrent.tracker.TorrentRequest.RequestEvent;

/**
 * Author: chris.oudeman
 * Since: Jan 13, 2011
 */
public class TrackerUdpRequest implements TorrentRequest{
	
	private final static long DEFAULT_CONNECTION_ID = 0x41727101980l;
	private final static int ACTION_CONNECT = 0x00;
	private final static int ACTION_ANNOUNCE = 0x01;
	private final static int ACTION_ERROR = 0x03;

	private final String trackerHost;
	private final int trackerPort;
	private final int transactionId;
	private final Random random;
	private final Sha1Hash infoHash;
	private final Sha1Hash peerId;
	private final long uploaded;
	private final long downloaded;
	private final long left;
	private final RequestEvent event;
	private final PeerAddress myAddress;
	
	private long connectionId;

	/**
	 * Creates a new {@link TrackerUdpRequest}. <br/>
	 * Throws a {@link MalformedURLException} when the tracker url does not appear
	 * to be valid.
	 * @param trackerUrl the UDP tracker url, e.g. udp://my.tracker:6789
	 * @throws {@link MalformedURLException} when the tracker url does not appear
	 * to be valid.
	 * 
	 * @throws IOException if the provided ipAddress and port or not valid...
	 */
	protected TrackerUdpRequest(String trackerUrl,
			Sha1Hash infoHash,
			Sha1Hash peerId,
			long uploaded,
			long downloaded,
			long left,
			RequestEvent event,
			String ipAddress,
			int port) throws MalformedURLException, IOException {

		///// Quickly validates whether this LOOKS like an UDP tracker url..
		if (!trackerUrl.startsWith("udp://")
				|| trackerUrl.indexOf(':', 6) == -1) {

			throw new MalformedURLException("Malformed UDP url: " + trackerUrl);
		}

		this.trackerHost = trackerUrl.substring(6, trackerUrl.indexOf(':', 6));
		try {
			this.trackerPort =
					Integer.parseInt(trackerUrl.substring(trackerUrl.indexOf(
							':',
							6) + 1));
		} catch (NumberFormatException nfe) {
			throw new MalformedURLException("Malformed UDP url (port): "
					+ trackerUrl);
		}
		if (trackerPort < 1 || trackerPort > 65535) {
			throw new MalformedURLException(
					"Malformed UDP url (invalid port): " + trackerUrl);
		}

		random = new Random();
		transactionId = random.nextInt();
		
		this.infoHash = infoHash;
		this.peerId = peerId;
		this.uploaded = uploaded;
		this.downloaded = downloaded;
		this.left = left;
		this.event = event;
		myAddress = new PeerAddress(ipAddress, port);

	}

	/**
	 * 
	 * @throws SecurityException if a datagram socket could not be opened due to a {@link SecurityException}, see: {@link DatagramSocket#DatagramSocket(int)}
	 * @throws SocketException if a datagram socket could not be opened.
	 * @throws IllegalArgumentException if parameter timeout if smaller or equal than 0.
	 * @see #openRx()
	 */
	@Override
	public TorrentResponse execute(int timeoutInMillis) throws IOException {

		if (timeoutInMillis <= 0) {
			throw new IllegalArgumentException(
					"Timeout must be larger than 0: " + timeoutInMillis);
		}

		DatagramSocket socket = openRx();
		socket.
	}

	/**
	 * Opens an <em>incoming</em> UDP {@link DatagramSocket}. <br/>
	 * {@link #openRx()} tries to open a {@link DatagramSocket} on port 15000 to 30000.<br/>
	 * If the socket could not be open due to a {@link SocketException} being throw, it tries again, 
	 * for a maximum of 100 times, wherafter it will throw an {@link SocketException}.<br/>
	 * If opning the socket will result in a {@link SecurityException}, it will rethrow the exception.
	 * 
	 * @throws SecurityException - When datagram socket could not be opened due to a {@link SecurityException}
	 * @htrows {@link SocketException} - If it could not open a port between 15000 and 30000 after trying 100 times.
	 */
	private DatagramSocket openRx() throws SocketException, SecurityException {



		for (int i = 0; i < 100; i++) { //// Try opning a UDP socket 100 times before giving up ...
			try {
				int port = 15000 + random.nextInt(15000); ///// Somewhere between 15000-30000
				return new DatagramSocket(port);
			} catch (SocketException se) {
				// port in use
			} catch (SecurityException se) {
				// UDP listening not allowed:
				throw se;
			}
		}

		throw new SocketException("Could not open a DatagramSocket");

	}

	/**
	 * Offset  Size            Name            Value
	0       64-bit integer  connection_id   0x41727101980
	8       32-bit integer  action          0 // connect
	12      32-bit integer  transaction_id
	16

	 */
	private byte[] createConnectRequest() {

		ByteBuffer bBuffer = ByteBuffer.allocate(16);
		bBuffer.putLong(DEFAULT_CONNECTION_ID);
		bBuffer.putInt(ACTION_CONNECT);
		bBuffer.putInt(transactionId);
		return bBuffer.array();
	}

	private void processConnectResponse(DatagramPacket packet) throws IOException{

		ByteBuffer bBuffer =
				ByteBuffer.wrap(packet.getData(), 0, packet.getLength());
		// check action & transaction id 
		switch (bBuffer.getInt()) {
		case ACTION_CONNECT:
			break;
		default:
			throw new IOException("Expected connect action");
		case ACTION_ERROR:
			bBuffer.getInt();//transaction id...
			throw new IOException("Received error: "
					+ new String(
							bBuffer.array(),
							bBuffer.position(),
							bBuffer.remaining()));

		}

		if (bBuffer.getInt() != transactionId) {
			throw new IOException("Transaction ID does not match expected.");
		}
		// set connection id..
		connectionId = bBuffer.getLong();
	}
	
	private byte[] createAnnounceRequest() {

		ByteBuffer bBuffer = ByteBuffer.allocate(98);
		bBuffer.putLong(connectionId);
		bBuffer.putInt(ACTION_ANNOUNCE);
		bBuffer.putInt(transactionId);
		bBuffer.put(infoHash.getHash());
		bBuffer.put(peerId.getHash());
		bBuffer.putLong(downloaded);
		bBuffer.putLong(left);
		bBuffer.putLong(uploaded);
		bBuffer.putInt(getEventValue());
		byte[] binIpAddress = myAddress.toByteString();
		bBuffer.put(binIpAddress, 0, 4);// ip
		bBuffer.putInt(0);//key
		bBuffer.putInt(50);//num_want
		bBuffer.put(binIpAddress, 4, 2); //port..
		
		return bBuffer.array();

	}

	private int getEventValue() {

		switch (event) {
		default:
		case NONE:
			return 0;
		case COMPLETED:
			return 1;
		case STARTED:
			return 2;
		case STOPPED:
			return 3;
		}
	}

}


